﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;

namespace GoodStuff.Web.Controls
{
    /// <summary>
    /// A custom column that act as 'totalizer'footercolumn within a GridView. It can show the sum, average or count of a column.
    /// </summary>
    /// <see cref="http://www.codeproject.com/KB/aspnet/SumColumn.aspx"/>
    public class SumField : BoundField
    {
        /// <summary>
        /// Constructor
        /// </summary>
        public SumField()
        {
            this.ShowSum = true;
            this.ShowAverage = false;
            this.ShowCount = false;
            this.AverageBasedOnNonNullValuesOnly = false;

            this.LabelAverageLineBreak = false;
            this.LabelCountLineBreak   = false;
            this.LabelSumLineBreak     = false;

            this.AverageNoDataLabel = "0";

            this.Encode = false;
        }

        /// <summary>
        /// Holder for summing of the values of each row/column
        /// </summary>
        private decimal _internalSum;

        /// <summary>
        /// Holder for the amount of items/rows. row/column without value is counted as well.
        /// </summary>
        private int _internalCount;

        /// <summary>
        /// Holder for the amount of items/rows which contain a value (Not Null)
        /// </summary>
        private int _internalCountNotNull;

        /// <summary>
        /// Show the sum of a column (default = true)
        /// </summary>
        public bool ShowSum { get; set; }

        /// <summary>
        /// Show to count of a column (default = false)
        /// </summary>
        public bool ShowCount { get; set; }

        /// <summary>
        /// Show to average of a column (default = false)
        /// </summary>
        public bool ShowAverage { get; set; }

        /// <summary>
        /// The prefix for the sum value
        /// </summary>
        public string LabelSum { get; set; }

        /// <summary>
        /// Insert a linebreak (BR) between label and value? (default = false)
        /// </summary>
        public bool LabelSumLineBreak { get; set; }

        /// <summary>
        /// The prefix for the average value
        /// </summary>
        public string LabelAverage { get; set; }

        /// <summary>
        /// Insert a linebreak (BR) between label and value? (default = false)
        /// </summary>
        public bool LabelAverageLineBreak { get; set; }

        /// <summary>
        /// The prefix for the count value
        /// </summary>
        public string LabelCount { get; set; }

        /// <summary>
        /// Insert a linebreak (BR) between label and value? (default = false)
        /// </summary>
        public bool LabelCountLineBreak { get; set; }

        /// <summary>
        /// False = Average based on itemcount/rows regardless it contains a value or not. True = Only row with a value (also '0') will be used in the average. NULL values while be ignored. Default = false.
        /// </summary>
        public bool AverageBasedOnNonNullValuesOnly { get; set; }

        /// <summary>
        /// A string to display in case no items are available to calculate the average. This string is displayed to avoid a division by zero exception. 
        /// </summary>
        /// <value>
        /// The string that is displayed when no average could be determined. Default value is "0".
        /// </value>
        public string AverageNoDataLabel { get; set; }

        /// <summary>
        /// The position to round on. Default is no rounding.
        /// </summary>
        public int? RoundOn { get; set; }

        /// <summary>
        /// The CSS class to apply to this specific column. Will be applied to the cell.
        /// </summary>
        public string CssClass { get; set; }

        /// <summary>
        /// Encode labels and values? (default = false)
        /// </summary>
        public bool Encode { get; set; }

        /// <summary>
        /// Override the InitializeCell of the base BoundField
        /// </summary>
        /// <param name="cell"></param>
        /// <param name="cellType"></param>
        /// <param name="rowState"></param>
        /// <param name="rowIndex"></param>
        public override void InitializeCell(System.Web.UI.WebControls.DataControlFieldCell cell, System.Web.UI.WebControls.DataControlCellType cellType, System.Web.UI.WebControls.DataControlRowState rowState, int rowIndex)
        {
            base.InitializeCell(cell, cellType, rowState, rowIndex);

            switch (cellType)
            {
                //checking for item type as we deal with the following only
                case DataControlCellType.DataCell:
                case DataControlCellType.Footer:
                    //as we cannot get the value of cell at the time of cell creation
                    //we are attaching a handler on the fly to the cell DataBinding event
                    cell.DataBinding += CellItemDataBound;
                    break;
            }
        }

        /// <summary>
        /// Eventhandler for each cell that is bounded.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void CellItemDataBound(object sender, EventArgs e)
        {
            TableCell cell = (TableCell)sender;
            GridViewRow row = (GridViewRow)cell.NamingContainer;
            //it may be a DataTable Row or a custom object
            object dataItem = row.DataItem;

            switch (row.RowType)
            {
                case DataControlRowType.DataRow:
                    //designMode property is used to handle the designTime layout of the SumColumn
                    if (this.DesignMode == false)
                    {
                        if (dataItem != null)
                        {
                            decimal? dValue = this.GetUnderlyingValue(dataItem);

                            //calling the base class method to format the value                    
                            cell.Text = this.FormatDataValue(dValue, this.Encode);

                            //add the value to the running total, in case we have a value
                            if (dValue.HasValue)
                            {
                                _internalSum += dValue.Value;

                                //incrementing count of items WITH a value
                                _internalCountNotNull += 1;
                            }
                        }

                        //incrementing count of items (regardsless if it has a value or not)
                        _internalCount += 1;
                    }
                    else
                    {
                        cell.Text = "SumColumn";
                    }

                    break;
                case DataControlRowType.Footer:
                    if (this.DesignMode == false)
                    {
                        //set the running total
                        if (this.ShowSum == true)
                        {
                            decimal finalValue = _internalSum;
                            if (this.RoundOn.HasValue)
                            {
                                finalValue = Math.Round(finalValue, this.RoundOn.Value);
                            }

                            if (string.IsNullOrEmpty(this.LabelSum))
                            {
                                cell.Text = this.FormatDataValue(finalValue, this.Encode);
                            }
                            else
                            {
                                string seperator = this.LabelSumLineBreak ? "<br/>" : ": ";
                                cell.Text = this.LabelSum + seperator + this.FormatDataValue(finalValue, this.Encode);
                            }
                        }

                        if (this.ShowCount == true)
                        {
                            decimal finalValue = _internalCount;
                            if (this.RoundOn.HasValue)
                            {
                                finalValue = Math.Round(finalValue, this.RoundOn.Value);
                            }

                            if (string.IsNullOrEmpty(this.LabelCount))
                            {
                                cell.Text += this.FormatDataValue(finalValue, this.Encode);
                            }
                            else
                            {
                                string seperator = this.LabelCountLineBreak ? "<br/>" : ": ";
                                cell.Text += this.LabelCount + seperator + finalValue;
                            }
                        }

                        if (this.ShowAverage == true)
                        {
                            //we use the internalCountNotNull as we want the average of the items that has an explicit value
                            //So "0" is counting, while "no value / NULL" isn't counted and is not part of the average

                            string valueToDisplay;

                            int itemCountForAverage = this.AverageBasedOnNonNullValuesOnly ? _internalCountNotNull : _internalCount;
                            if (itemCountForAverage > 0)
                            {
                                decimal finalValue = _internalSum / itemCountForAverage;
                                if (this.RoundOn.HasValue)
                                {
                                    finalValue = Math.Round(finalValue, this.RoundOn.Value);
                                }

                                valueToDisplay = this.FormatDataValue(finalValue, this.Encode);
                            }
                            else
                            {
                                valueToDisplay = this.AverageNoDataLabel;
                            }

                            if (string.IsNullOrEmpty(this.LabelAverage))
                            {
                                cell.Text += valueToDisplay;
                            }
                            else
                            {
                                string seperator = this.LabelAverageLineBreak ? "<br/>" : ": ";
                                cell.Text += this.LabelAverage + seperator + valueToDisplay;
                            }
                        }
                    }
                    else
                    {
                        cell.Text = "Total";
                    }

                    //For long tables, append the original footertext
                    if (!string.IsNullOrEmpty(this.FooterText))
                    {
                        cell.Text = this.FooterText + "<br/>" + cell.Text;
                    }

                    break;
            }
        }

        /// <summary>
        /// GetUnderlyingValue
        /// </summary>
        /// <param name="dataItem"></param>
        /// <returns></returns>
        protected decimal? GetUnderlyingValue(object dataItem)
        {

            //checking whether then mentioned field exist or not in the underlying DataSource object
            PropertyDescriptor boundFieldDesc = TypeDescriptor.GetProperties(dataItem).Find(this.DataField, true);

            //if not then throw exception otherwise return the value
            if (boundFieldDesc == null)
            {
                throw new HttpException("Field Not Found: " + this.DataField);
            }

            object dValue = boundFieldDesc.GetValue(dataItem);

            if (dValue == null || dValue == DBNull.Value)
            {
                return null;
            }

            //Parse non-null-value to decimal
            try
            {
                return decimal.Parse(dValue.ToString());
            }
            catch (FormatException fEx)
            {
                throw new FormatException("Probably a non numeric datafield has been configured for SumField " + this.DataField, fEx);
            }
            catch
            {
                throw;
            }
        }
    }
}
